package com.alkalinesolutions.json.compat;

import java.io.Serializable;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;

import org.json.JSONArray;
import org.json.JSONObject;

import com.alkalinesolutions.json.core.CyclicalStructureException;
import com.alkalinesolutions.json.core.UnsupportedTypeException;
import com.alkalinesolutions.json.core.impl.JSONEvent;
import com.alkalinesolutions.json.core.impl.JSONType;
import com.alkalinesolutions.json.core.impl.JSONEvent.EventType;

/** Walk a JSONArray or JSONObject, returning an object exposing a stream of JSONEvents
 * 
 * @author David Waite &lt;<a href='mailto:david@alkaline-solutions.com'>david@alkaline-solutions.com</a>&gt;
 */
public class OrgJsonWalker   {
	
	private OrgJsonWalker() { /* do not allow creation */}
	
	public static Iterable<JSONEvent> walk(final Object source, final JSONArray array) {
		InternalWalker result = new InternalWalker(source, array, new JSONArrayIterator(array));
		return result;
	}
	public static Iterable<JSONEvent> walk(final Object source, final JSONObject object) {
		InternalWalker result = new InternalWalker(source, object, new JSONObjectIterator(object));
		return result;
	}

	private static class DataEntry {
		public Object      data;
		public Iterator<?> iterator;
		
		public DataEntry(Object data, Iterator<?> iterator) {
			this.data = data;
			this.iterator = iterator;
		}
		
		public EventType getStartEventType() {
			return data instanceof JSONObject 
				? EventType.StartObject : EventType.StartArray;
		}
		public EventType getEndEventType() {
			return data instanceof JSONObject
			? EventType.EndObject : EventType.EndArray;
		}
	}

	protected static class InternalWalker implements Iterator<JSONEvent>, Iterable<JSONEvent> {

		/** stack alternates between a struct type and an iterator. The topmost iterator is instead stored in 'iterator' for efficiency */
		private final Object source;

		private final Stack< DataEntry > stack;
		private boolean first;
		
		InternalWalker(Object source, Object data, Iterator<?> iterator) {
			if (source == null)
				throw new IllegalArgumentException("source"); //$NON-NLS-1$

			this.source = source;
			stack = new Stack< DataEntry >();
			stack.push(new DataEntry(data, iterator));
			first = true;
		}
		private void scanStackForSameObject(Object data) throws CyclicalStructureException {
			for(DataEntry dw : stack) {
				if (data == dw.data)
					throw new CyclicalStructureException(data);
			}
		}
		public boolean hasNext() {
			return ( first || !stack.isEmpty());
		}
	
		public JSONEvent next() {
			try {
				/* if this is the first time iterating into the data entry at
				 * the top of the stack, return a start event.
				 */
				if (first) {
					first = false;
	
					return new JSONEvent(source, stack.peek().getStartEventType(), null, null);
				}
				if (!hasNext())
					throw new NoSuchElementException();
		
				/* clear off data entries one by one (each iteration) if they
				 * have no more entries in them.
				 */
				DataEntry dw = stack.peek();
				if (!dw.iterator.hasNext()) {
					stack.pop();
					return new JSONEvent(source, dw.getEndEventType(), null, null);
				}
		
				/*
				 * Grab the next entry and determine key and data
				 */
				String key = null;
				Object data = dw.iterator.next();
				if (data instanceof Map.Entry<?, ?>) {
					@SuppressWarnings("unchecked")
					Map.Entry<String, Object> entry = (Map.Entry<String, Object>) data;
					key = entry.getKey();
					data = entry.getValue();
				}

				if (data instanceof JSONObject) {
					scanStackForSameObject(data);
					stack.push(new DataEntry(data, new JSONObjectIterator((JSONObject)data)));
					return new JSONEvent(source, EventType.StartObject, null, key);					
				}
				if (data instanceof JSONArray) {
					scanStackForSameObject(data);
					stack.push(new DataEntry(data, new JSONArrayIterator((JSONArray)data)));
					return new JSONEvent(source, EventType.StartArray, null, key);
				}
				if (JSONObject.NULL.equals(data)) {
					return new JSONEvent(source, EventType.Null, null, key);
				}
				switch (JSONType.valueFromObject(data)) {
				case Boolean:
					return new JSONEvent(source, EventType.Boolean, (Serializable) data, key);
				case Number:
					return new JSONEvent(source, EventType.Number, (Serializable) data, key);
				case String:
					return new JSONEvent(source, EventType.String, (Serializable) data, key);
				case Null:
				case Array:
				case List:
				case Object:
					/* none of these are legal matches */
					throw new IllegalStateException(Messages.getString("OrgJsonWalker.0")); //$NON-NLS-1$
				default:
					throw new IllegalStateException();
				}
			} catch (UnsupportedTypeException e) {
				throw new UndeclaredThrowableException(e);
			}
		}
	
		public void remove() {
			throw new UnsupportedOperationException();
		}
		
		public Iterator<JSONEvent> iterator() {
			return this;
		}
	}
}
